from bs4 import BeautifulSoup
import re
import requests

from pprint import pprint


def debug(val):
    print("\n---------- DEBUG ----------------\n")
    pprint(val)
    print("\n")


class ContentNotAvailable(Exception):
    pass


class Base(object):

    def __init__(self, *args, **kwargs):
        self._result = {}  # cached results dict of "username: data"

    def _uri(self, val):
        raise NotImplementedError("Should be implemented by a subclass")

    def _soupify(self, content):
        #if not self._soup:
            #self._soup = BeautifulSoup(content)
        #return self._soup
        return BeautifulSoup(content)

    def _parse_sibling(self, heading, role, next_sibling="p"):
        """Given a BeautifulSoup representation of a heading, find the very
        next sibling (the next <p>, by default), and look for all <a> children;

        e.g. pull the links out of an element containing the following::

            <p class="OverflowHidden">
                <a href=".." title="...">...<br/></a>
                <a href=".." title="...">...<br/></a>
            </p>


        Return a list of dictionaries corresponding to the child anchors (<a>)
        in the following format::

            [
                {"role": (input role), "name": (a.title), "url": (a.href)},
                {"role": (input role), "name": (a.title), "url": (a.href)},
            ]

        """
        projects = []
        sibling = heading.find_next_sibling(next_sibling)
        if sibling:
            children = sibling.findChildren("a") or []
            for a in children:
                projects.append({
                    'role': role,
                    'name': a.attrs['title'],
                    'url': a.attrs['href']
                })
        return projects

    def _parse_user_links(self, soup, id_attr):
        """Search for an element by its "id" attribute, and search for all
        child links."""
        items = []
        elt = soup.select("#{0}".format(id_attr))[0]
        if elt:
            children = elt.findChildren("a") or []
            for a in children:
                # <a href="http://www.codeplex.com/site/users/view/billchiles"
                #    target="_blank" class="UserAvatarLink">billchiles</a>
                items.append({
                    'url': a['href'],
                    'username': ''.join(a.stripped_strings),
                })
        return items


class User(Base):

    def _uri(self, username):
        pattern = "http://www.codeplex.com/site/users/view/{0}"
        return pattern.format(username)

    def _projects(self, content):
        """parse the html content and find a list of projects."""
        projects = []
        soup = self._soupify(content)
        for h in soup.findAll('h2'):
            heading = h.string
            if heading and heading.lower().strip() == "coordinator for":
                # <h2>coordinator for</h2>
                projects.extend(self._parse_sibling(h, 'coordinator'))
            elif heading and heading.lower().strip() == "editor for":
                # <h2>editor for</h2>
                projects.extend(self._parse_sibling(h, 'editor'))
            elif heading and heading.lower().strip() == "developer for":
                # <h2>developer for</h2>
                projects.extend(self._parse_sibling(h, 'developer'))
        return projects

    def _forks(self, content):
        """parse the html content and find a list of projects that the user
        has forked."""
        forks = []
        soup = self._soupify(content)
        for h in soup.findAll('h2'):
            heading = h.string
            if heading and heading.lower().strip() == "forks":
                # <h2>forks</h2>
                forks.extend(
                    self._parse_sibling(h, 'forked', next_sibling='div')
                )
        return forks

    def get(self, username):
        if username not in self._result:
            response = requests.get(self._uri(username))
            if response.status_code == 200:
                self._result[username] = {
                    'projects': self._projects(response.content),
                    'forks': self._forks(response.content),
                }
            else:
                raise ContentNotAvailable
        return self._result[username]


class Project(Base):

    def _uri(self, slug, path=''):
        pattern = "http://{0}.codeplex.com/{1}"
        return pattern.format(slug, path)

    def _downloads(self, content):
        soup = self._soupify(content)

        # Find the <div> containing a table of various info (among them
        # downloads & ratings)
        d = soup.select("#current_rating")[0]

        # The Total number of downloads is in the 3rd table cell
        value = d.find_all('td')[3].string

        # get rid of any non-digit characters
        value = re.sub('[^0-9]+', '', value)
        return int(value)

    def _rating(self, content):
        soup = self._soupify(content)

        # Current Rating is stored in a hidden input:
        # <input id="releasecurrentRating" type="hidden" value="5">
        elt = soup.select("#releasecurrentRating")[0]
        return int(elt.attrs['value'])

    def _forks(self, content):
        # TODO: Where was this? Was I dreaming?
        raise NotImplementedError

    def _followers(self, slug):
        """Pull the number of followers from the content of the "Follow" link:

        Send a GET request to (<p> == the project slug):

            http://<p>.codeplex.com/site/api/projects/<p>/followProject

        Returns JSON:

            {"TotalFollowers":748,"IsFollowing":false}

        """
        follower_path = "site/api/projects/{0}/followProject".format(slug)
        uri = self._uri(slug, path=follower_path)
        resp = requests.get(uri)
        if resp.status_code != 200:
            raise ContentNotAvailable

        if callable(resp.json):
            data = resp.json()
        else:
            data = resp.json
        return data['TotalFollowers']

    def _coordinators(self, content):
        soup = self._soupify(content)
        return self._parse_user_links(soup, "CoordinatorsContainer")

    def _editors(self, content):
        soup = self._soupify(content)
        return self._parse_user_links(soup, "EditorsContainer")

    def _developers(self, content):
        soup = self._soupify(content)
        return self._parse_user_links(soup, "DevelopersContainer")

    def get(self, slug):
        """NOTE: getting this data takes 3 http request."""
        if slug not in self._result:

            # 1. Get the content available from the project's Home page
            response = requests.get(self._uri(slug))
            if response.status_code == 200:
                self._result[slug] = {
                    'downloads': self._downloads(response.content),
                    'rating': self._rating(response.content),
                }
            else:
                raise ContentNotAvailable

            # 2. Sigh. Number of followers is a JSON endpoint.
            self._result[slug].update({
                'followers': self._followers(slug)
            })

            # 3. Get the content from the project's Team Page
            response = requests.get(self._uri(slug, path="team/view"))
            if response.status_code == 200:
                self._result[slug].update({
                    'team': {
                        'coordinators': self._coordinators(response.content),
                        'editors': self._editors(response.content),
                        'developers': self._developers(response.content),
                    }
                })
            else:
                raise ContentNotAvailable
        return self._result[slug]


def get_user():
    return User()


def get_project():
    return Project()
